package junior.db.dao.base;

import com.google.common.collect.Maps;
import junior.db.annotation.PersistentValue;
import junior.db.annotation.Type;
import junior.db.annotation.TypeEntity;
import junior.db.dao.mybatis.base.IBaseMapper;
import junior.db.dao.mybatis.type.TypeValue;
import junior.db.mapping.MappingProvider;
import junior.db.query.QueryHelper;
import junior.util.exception.ExceptionUtils;
import junior.util.reflect.ReflectUtils;
import junior.util.string.StringPlaceHolderUtils;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;

public abstract class MybatisHelper {
    
    public static Class<?> getMapperEntityClass(Class<?> mapperClass) {
        Class<?> entityClass = ReflectUtils.getInterfaceGenericType(mapperClass, IBaseMapper.class, 0);
        if (entityClass == null) {
            entityClass = ReflectUtils.getSuperClassGenericType(mapperClass, 0);
        }
        return entityClass;
    }
    
    /**
     * 根据现有的 ms 创建一个新的，使用新的返回值类型
     *
     * @param ms
     * @return
     */
    public static MappedStatement newMappedStatement(MappedStatement ms, Class<?> resultType, List<ResultMap> resultMaps) {
        //下面是新建的过程，考虑效率和复用对象的情况下，这里最后生成的ms可以缓存起来，下次根据 ms.getId() + "_" + getShortName(resultType) 直接返回 ms,省去反复创建的过程
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId() + "_" + getShortName(resultType), ms.getSqlSource(), ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
            StringBuilder keyProperties = new StringBuilder();
            for (String keyProperty : ms.getKeyProperties()) {
                keyProperties.append(keyProperty).append(",");
            }
            keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
            builder.keyProperty(keyProperties.toString());
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        
        builder.resultMaps(resultMaps);
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }
    
    public static Class<?> getResultType(MappedStatement ms) {
        if (!ms.getResultMaps().isEmpty()) {
            ResultMap resultMap = ms.getResultMaps().get(0);
            if (resultMap != null) {
                return resultMap.getType();
            }
        }
        return null;
    }
    
    private static String getShortName(Class<?> clazz){
        String className = clazz.getCanonicalName();
        return className.substring(className.lastIndexOf(".") + 1);
    }
    
    private static Map<Class<?>, String[]> selectFieldsMap = new HashMap<Class<?>, String[]>();
    
    public static String getTable(String namespace, Class<?> entityClass) {
        Assert.notNull(MappingProvider.getTableMapping(namespace), "TableMapper cannot be null");
        return MappingProvider.getTableMapping(namespace).entity2TableName(entityClass);
    }
    
    public static String getSelectColumns(String namespace, Class<?> poClass, String prefix) {
        String[] fieldNames = selectFieldsMap.get(poClass);
        if (fieldNames == null) {
            fieldNames = QueryHelper.getDBFieldNames(poClass, null);
            
            selectFieldsMap.put(poClass, fieldNames);
        }
        
        return StringUtils.arrayToDelimitedString(fields2Columns(namespace, poClass, fieldNames, prefix), ", ");
    }
    
    public static String[] fields2Columns(String namespace, Class<?> poClass, String[] fieldNames) {
        return fields2Columns(namespace, poClass, fieldNames, null);
    }
    
    public static String[] fields2Columns(String namespace, Class<?> poClass, String[] fieldNames, String prefix) {
        Assert.notNull(MappingProvider.getColumnMapping(namespace), "ColumnMapper cannot be null");
        String[] cols = new String[fieldNames.length];
        for (int i=0; i<fieldNames.length; i++) {
            cols[i] = field2Column(namespace, fieldNames[i], prefix, (p) -> poClass);
        }
        return cols;
    }
    
    public static String field2Column(String namespace, Class<?> entityClass, String field) {
        return field2Column(namespace, field, "", (p) -> entityClass);
    }
    
    public static String field2Column(String namespace, String field, Function<String, Class<?>> entityClassByPrefix) {
        return field2Column(namespace, field, "", entityClassByPrefix);
    }
    
    public static String field2Column(String namespace, Class<?> entityClass, String field, String prefix) {
        return field2Column(namespace, field, prefix, (p) -> entityClass);
    }
    
    public static String field2Column(String namespace, String field, String prefix, Function<String, Class<?>> entityClassByPrefix) {
        Assert.notNull(MappingProvider.getColumnMapping(namespace), "ColumnMapper cannot be null");
        
        Set<String> placeHolders = StringPlaceHolderUtils.getPlaceHolders(field);
        if (placeHolders.isEmpty()) {
            int index = field.indexOf(".");
            if (index != -1) {
                prefix = field.substring(0, index + 1);
                field = field.substring(index + 1);
            }
            
            String column = MappingProvider.getColumnMapping(namespace).field2Column(entityClassByPrefix.apply(prefix), field);
            if (QueryHelper.hasFunction(column)) {
                return column;
            }
            
            String p = prefix != null ? prefix : "";
            if (p.length() > 0 && ! p.endsWith(".")) p = p + ".";
            
            return QueryHelper.hasPrefix(column) ? column : p + column;
        }
        
        Map<String, String> phMap = Maps.newHashMap();
        for (String ph : placeHolders) {
            doFieldAndPrefix(ph, prefix, (f, cp) -> {
                String column = MappingProvider.getColumnMapping(namespace).field2Column(entityClassByPrefix.apply(cp), f);
                if (cp != null) {
                    if (cp.length() > 0 && ! cp.endsWith(".")) {
                        cp = cp + ".";
                    }
                } else {
                    cp = "";
                }
                phMap.put(ph, cp + column);
            });
        }
        return StringPlaceHolderUtils.replace(field, phMap);
    }
    
    public static String field2Expression(String namespace, String sql, Function<String, Class<?>> entityClassByPrefix, Function<String, String> fieldValueFunc) {
        Set<String> fields = StringPlaceHolderUtils.getPlaceHolders(sql);
        if (! fields.isEmpty()) {
            Map<String, String> placeHolders = Maps.newHashMapWithExpectedSize(fields.size());
            for (String f : fields) {
                placeHolders.put(f, MybatisHelper.field2Column(namespace, f, entityClassByPrefix));
            }
            sql = StringPlaceHolderUtils.replace(sql, placeHolders);
        } else {
            if (QueryHelper.isField(sql)) {
                sql = MybatisHelper.field2Column(namespace, sql, entityClassByPrefix);
            }
        }
        
        Set<String> valueExpressions = StringPlaceHolderUtils.getPlaceHolders(sql, "#{", "}");
        if (! fields.isEmpty()) {
            Map<String, String> placeHolders = Maps.newHashMapWithExpectedSize(fields.size());
            for (String f : valueExpressions) {
                placeHolders.put(f, fieldValueFunc.apply(f));
            }
            sql = StringPlaceHolderUtils.replace(sql, placeHolders);
        }
        return sql;
    }
    
    public static SqlValue sqlToSqlValue(String namespace, Class<?> entityClass, String sql, Function<String, Object> fieldValueFunc) {
        SqlValue sqlValue = new SqlValue();
        Set<String> fields = StringPlaceHolderUtils.getPlaceHolders(sql);
        if (! fields.isEmpty()) {
            Map<String, String> placeHolders = Maps.newHashMapWithExpectedSize(fields.size());
            for (String f : fields) {
                placeHolders.put(f, MybatisHelper.field2Column(namespace, entityClass, f));
            }
            sql = StringPlaceHolderUtils.replace(sql, placeHolders);
        } else {
            if (QueryHelper.isField(sql)) {
                sql = MybatisHelper.field2Column(namespace, entityClass, sql);
            }
        }
        StringPlaceHolderUtils.parsePlaceHolders(sql, "#{", "}", (f) -> {
            sqlValue.arg(fieldValueFunc.apply(f));
        }, (s) -> {
            sqlValue.sql(s);
        });
        return sqlValue;
    }
    
    public static String getCustomSql(String namespace, String customSql, String prefix, Function<String, Class<?>> entityClassByPrefix) {
        Set<String> placeHolders = StringPlaceHolderUtils.getPlaceHolders(customSql);
        if (placeHolders.isEmpty()) {
            return customSql;
        }
        Map<String, String> phMap = Maps.newHashMap();
        for (String ph : placeHolders) {
            doFieldAndPrefix(ph, prefix, (f, cp) -> {
                String column = MappingProvider.getColumnMapping(namespace).field2Column(entityClassByPrefix.apply(cp), f);
                if (cp != null) {
                    if (cp.length() > 0 && ! cp.endsWith(".")) {
                        cp = cp + ".";
                    }
                } else {
                    cp = "";
                }
                phMap.put(ph, cp + column);
            });
        }
        return StringPlaceHolderUtils.replace(customSql, phMap);
    }
    
    private static void doFieldAndPrefix(String field, String defaultPrefix, BiConsumer<String, String> fieldAndPrefix) {
        int index = field.indexOf(".");
        String cp = defaultPrefix;
        if (index != -1) {
            cp = field.substring(0, index + 1);
            if (fieldAndPrefix != null) {
                fieldAndPrefix.accept(field.substring(index + 1), cp);
            }
        } else {
            fieldAndPrefix.accept(field, cp);
        }
    }
    
    public static Object getFieldValue(Object entity, String field) {
        if (entity instanceof Map) {
            return ((Map) entity).get(field);
        }
        try {
            Method getter = ReflectUtils.getter(entity.getClass(), field);
            Object value = getter.invoke(entity);
            if (value != null) {
                String type = getFieldType(entity.getClass(), field);
                if (type != null) {
                    return new TypeValue(value, type, getter.getReturnType());
                }
                Class<?> valueType = value.getClass();
                if (valueType.isAnnotationPresent(PersistentValue.class)) {
                    PersistentValue pv = valueType.getAnnotation(PersistentValue.class);
                    try {
                        Method m = ReflectUtils.getDeclaredMethod(valueType, pv.toMethod(), true);
                        value = m.invoke(value);
                    } catch (Exception e) {
                        throw ExceptionUtils.wrap2Runtime(e);
                    }
                }
            }
            return value;
        } catch (Exception e) {
            throw ExceptionUtils.wrap2Runtime(e);
        }
    }
    
    public static String getFieldType(Class<?> entityClass, String field) {
        if (entityClass.getAnnotation(TypeEntity.class) == null) {
            return null;
        }
        Field f = ReflectionUtils.findField(entityClass, field);
        if (f != null) {
            Type type = f.getAnnotation(Type.class);
            if (type != null) {
                return type.value();
            }
        }
        
        PropertyDescriptor pd = ReflectUtils.getPropertyDescriptor(entityClass, field);
        if (pd != null) {
            Method getter = pd.getReadMethod();
    
            if (pd.getWriteMethod() == null || getter == null) {
                return null;
            }
    
            Type type = getter.getAnnotation(Type.class);
            return type != null ? type.value() : null;
        }
        return null;
    }
}
